package com.google.code.jtasks.bugzilla.proxy.bug;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;

import com.google.code.jtasks.bugzilla.proxy.ProcessingUtils;
import com.google.code.jtasks.bugzilla.proxy.bug.BugSearchProxy.Bug;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

/**
 * This class provides a proxy for the <code>Bug.history</code> XML-RPC method.
 */
public final class BugHistoryProxy
{
	private final XmlRpcClient rpcClient;

	/**
	 * Constructs a new {@link BugHistoryProxy} instance.
	 * 
	 * @param rpcClient
	 *            the {@link XmlRpcClient} that should be used
	 */
	public BugHistoryProxy(XmlRpcClient rpcClient)
	{
		this.rpcClient = rpcClient;
	}

	/**
	 * This method is a proxy for the <code>Bug.history</code> XML-RPC method.
	 * 
	 * @param params
	 *            the {@link HistorySelector} that specifies which comments should be returned
	 * @return a {@link HistoryResult} object containing the results of the search
	 */
	public HistoryResult history(HistorySelector params)
	{
		Object rpcResultStruct;
		try
		{
			rpcResultStruct = rpcClient.execute("Bug.history", new Object[] { params.convertToRpcStruct() });
		}
		catch (XmlRpcException e)
		{
			// TODO throw a better exception
			throw new RuntimeException(e);
		}

		return new HistoryResult(rpcResultStruct);
	}

	/**
	 * Specifies the bugs that history should be returned for. Represents the parameters that can be passed to
	 * the <code>Bug.history</code> XML-RPC method.
	 */
	public static final class HistorySelector
	{
		private final Set<Integer> ids;

		/**
		 * Constructs a new {@link HistorySelector} instance.
		 */
		public HistorySelector()
		{
			this.ids = new HashSet<Integer>();
		}

		/**
		 * Filters the search results to bugs with the specified ID.
		 * 
		 * @param ids
		 *            the IDs of the bugs to include in the search results
		 */
		public void filterByIds(Collection<Integer> ids)
		{
			this.ids.clear();
			this.ids.addAll(ids);
		}

		/**
		 * Converts this {@link HistorySelector} instance to a "raw" struct that can be passed to an
		 * {@link XmlRpcClient}.
		 * 
		 * @return a "raw" struct that can be passed to an {@link XmlRpcClient}
		 */
		Map<String, Object> convertToRpcStruct()
		{
			Map<String, Object> rpcStruct = new HashMap<String, Object>();

			rpcStruct.put("ids", ProcessingUtils.toIntegerArray(ids));

			return rpcStruct;
		}
	}

	/**
	 * Represents the {@link HistoryEntry}s returned by the <code>Bug.comments</code> XML-RPC method.
	 */
	public static final class HistoryResult
	{
		/**
		 * A {@link Map} of {@link HistoryEntry}s, grouped by {@link Bug#getId()}.
		 */
		private final Multimap<Integer, HistoryEntry> history;

		/**
		 * Constructs a new {@link HistoryResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result object.
		 * 
		 * @param rpcResultStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result object to build the
		 *            {@link HistoryResult} from
		 */
		public HistoryResult(Object rpcResultStruct)
		{
			Map<?, ?> rootMap = ProcessingUtils.castToMap(rpcResultStruct);
			Object[] bugs = ProcessingUtils.extractObjectArrayField(rootMap, "bugs", false);

			// Walk through each bug in the array, extracting the comments in it
			this.history = ArrayListMultimap.create();
			for (Object untypedBug : bugs)
			{
				Map<?, ?> bugMap = ProcessingUtils.castToMap(untypedBug);
				Integer id = ProcessingUtils.extractIntegerField(bugMap, "id", false);
				String alias = ProcessingUtils.extractStringField(bugMap, "alias", true);
				Object[] history = ProcessingUtils.extractObjectArrayField(bugMap, "history", false);

				// Walk through each history entry for the current bug, adding it to the list
				for (Object historyEntryStruct : history)
				{
					HistoryEntry historyEntry = new HistoryEntry(historyEntryStruct);
					this.history.put(id, historyEntry);
				}
			}
		}

		/**
		 * Returns a {@link Multimap} of the {@link HistoryEntry}s contained in this {@link HistoryResult},
		 * where the map keys are the {@link Bug#getId()} of the {@link Bug} that the {@link HistoryEntry}s
		 * are associated with.
		 * 
		 * @return a {@link Multimap} of the {@link HistoryEntry}s contained in this {@link HistoryResult},
		 *         where the map keys are the {@link Bug#getId()} of the {@link Bug} that the
		 *         {@link HistoryEntry}s are associated with
		 */
		public Multimap<Integer, HistoryEntry> getHistory()
		{
			return history;
		}
	}

	/**
	 * Represents a Bugzilla {@link Bug} {@link HistoryEntry}, as returned by the <code>Bug.history</code>
	 * XML-RPC method.
	 */
	public static final class HistoryEntry implements Serializable
	{
		private static final long serialVersionUID = -7534127998887733287L;

		private final Date when;
		private final String who;
		private final List<Change> changes;

		/**
		 * Constructs a new {@link HistoryResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param historyEntryStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link HistoryEntry} from
		 */
		public HistoryEntry(Object historyEntryStruct)
		{
			Map<?, ?> rootHistoryMap = ProcessingUtils.castToMap(historyEntryStruct);

			this.when = ProcessingUtils.extractDateField(rootHistoryMap, "when");
			this.who = ProcessingUtils.extractStringField(rootHistoryMap, "who", false);
			Object[] changesStruct = ProcessingUtils
					.extractObjectArrayField(rootHistoryMap, "changes", false);

			// Walk through each history entry for the current bug, adding it to the list
			this.changes = new ArrayList<Change>();
			for (Object changeStruct : changesStruct)
			{
				Change change = new Change(changeStruct);
				this.changes.add(change);
			}
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#when
		 */
		public Date getWhen()
		{
			return when;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#who
		 */
		public String getWho()
		{
			return who;
		}

		/**
		 * Returns a {@link List} of the {@link Change}s in this {@link HistoryEntry}.
		 * 
		 * @return a {@link List} of the {@link Change}s in this {@link HistoryEntry}
		 */
		public List<Change> getChanges()
		{
			return Collections.unmodifiableList(changes);
		}
	}

	/**
	 * Represents a single field change in a Bugzilla {@link Bug} {@link HistoryEntry}, as returned by the
	 * <code>Bug.history</code> XML-RPC method.
	 */
	public static final class Change implements Serializable
	{
		private static final long serialVersionUID = -1705155309345767299L;

		private final String field_name;
		private final String removed;
		private final String added;
		private final Integer attachment_id;

		/**
		 * Constructs a new {@link HistoryResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param changeStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link HistoryEntry} from
		 */
		public Change(Object changeStruct)
		{
			Map<?, ?> rootChangeMap = ProcessingUtils.castToMap(changeStruct);

			this.field_name = ProcessingUtils.extractStringField(rootChangeMap, "field_name", false);
			this.removed = ProcessingUtils.extractStringField(rootChangeMap, "removed", false);
			this.added = ProcessingUtils.extractStringField(rootChangeMap, "added", false);
			this.attachment_id = ProcessingUtils.extractIntegerField(rootChangeMap, "attachment_id", true);
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#field_name
		 */
		public String getField_name()
		{
			return field_name;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#added
		 */
		public String getAdded()
		{
			return added;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#removed
		 */
		public String getRemoved()
		{
			return removed;
		}
	}
}
